/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.solr.handler;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.function.UnaryOperator;
import org.apache.solr.SolrTestCaseJ4;
import org.apache.solr.common.SolrException;
import org.apache.solr.common.SolrInputDocument;
import org.apache.solr.common.SolrInputField;
import org.apache.solr.common.params.ModifiableSolrParams;
import org.apache.solr.common.util.ContentStreamBase;
import org.apache.solr.common.util.Utils;
import org.apache.solr.handler.loader.JsonLoader;
import org.apache.solr.request.SolrQueryRequest;
import org.apache.solr.response.SolrQueryResponse;
import org.apache.solr.update.AddUpdateCommand;
import org.apache.solr.update.CommitUpdateCommand;
import org.apache.solr.update.DeleteUpdateCommand;
import org.apache.solr.update.processor.BufferingRequestProcessor;
import org.apache.solr.util.RandomNoReverseMergePolicyFactory;
import org.junit.BeforeClass;
import org.junit.ClassRule;
import org.junit.Test;
import org.junit.rules.TestRule;

public class JsonLoaderTest extends SolrTestCaseJ4 {

  @ClassRule
  public static final TestRule noReverseMerge = RandomNoReverseMergePolicyFactory.createRule();

  @BeforeClass
  public static void beforeTests() throws Exception {
    initCore("solrconfig.xml", "schema.xml");
  }

  static String input =
      json(
          "{\n"
              + "\n"
              + "'add': {\n"
              + "  'doc': {\n"
              + "    'bool': true,\n"
              + "    'f0': 'v0',\n"
              + "    'array': [ 'aaa', 'bbb' ]\n"
              + "  }\n"
              + "},\n"
              + "'add': {\n"
              + "  'commitWithin': 1234,\n"
              + "  'overwrite': false,\n"
              + "  'boost': 3.45,\n"
              + "  'doc': {\n"
              + "    'f1': 'v1',\n"
              + "    'f1': 'v2',\n"
              + "    'f2': null\n"
              + "  }\n"
              + "},\n"
              + "\n"
              + "'commit': {},\n"
              + "'optimize': { 'waitSearcher':false, 'openSearcher':false },\n"
              + "\n"
              + "'delete': { 'id':'ID' },\n"
              + "'delete': { 'id':'ID', 'commitWithin':500 },\n"
              + "'delete': { 'query':'QUERY' },\n"
              + "'delete': { 'query':'QUERY', 'commitWithin':500 },\n"
              + "'rollback': {}\n"
              + "\n"
              + "}\n"
              + "");

  public void testParsing() throws Exception {
    SolrQueryRequest req = req();
    SolrQueryResponse rsp = new SolrQueryResponse();
    BufferingRequestProcessor p = new BufferingRequestProcessor(null);
    JsonLoader loader = new JsonLoader();
    loader.load(req, rsp, new ContentStreamBase.StringStream(input), p);

    assertEquals(2, p.addCommands.size());

    AddUpdateCommand add = p.addCommands.get(0);
    assertEquals(
        "SolrInputDocument(fields: [bool=true, f0=v0, array=[aaa, bbb]])", add.solrDoc.toString());

    //
    add = p.addCommands.get(1);
    assertEquals("SolrInputDocument(fields: [f1=[v1, v2], f2=null])", add.solrDoc.toString());
    assertFalse(add.overwrite);

    // parse the commit commands
    assertEquals(2, p.commitCommands.size());
    CommitUpdateCommand commit = p.commitCommands.get(0);
    assertFalse(commit.optimize);
    assertTrue(commit.waitSearcher);
    assertTrue(commit.openSearcher);

    commit = p.commitCommands.get(1);
    assertTrue(commit.optimize);
    assertFalse(commit.waitSearcher);
    assertFalse(commit.openSearcher);

    // DELETE COMMANDS
    assertEquals(4, p.deleteCommands.size());
    DeleteUpdateCommand delete = p.deleteCommands.get(0);
    assertEquals(delete.id, "ID");
    assertNull(delete.query);
    assertEquals(delete.commitWithin, -1);

    delete = p.deleteCommands.get(1);
    assertEquals(delete.id, "ID");
    assertNull(delete.query);
    assertEquals(delete.commitWithin, 500);

    delete = p.deleteCommands.get(2);
    assertNull(delete.id);
    assertEquals(delete.query, "QUERY");
    assertEquals(delete.commitWithin, -1);

    delete = p.deleteCommands.get(3);
    assertNull(delete.id);
    assertEquals(delete.query, "QUERY");
    assertEquals(delete.commitWithin, 500);

    // ROLLBACK COMMANDS
    assertEquals(1, p.rollbackCommands.size());

    req.close();
  }

  public void testSimpleFormat() throws Exception {
    String str = "[{'id':'1'},{'id':'2'}]".replace('\'', '"');
    SolrQueryRequest req = req("commitWithin", "100", "overwrite", "false");
    SolrQueryResponse rsp = new SolrQueryResponse();
    BufferingRequestProcessor p = new BufferingRequestProcessor(null);
    JsonLoader loader = new JsonLoader();
    loader.load(req, rsp, new ContentStreamBase.StringStream(str), p);

    assertEquals(2, p.addCommands.size());

    AddUpdateCommand add = p.addCommands.get(0);
    SolrInputDocument d = add.solrDoc;
    SolrInputField f = d.getField("id");
    assertEquals("1", f.getValue());
    assertEquals(add.commitWithin, 100);
    assertFalse(add.overwrite);

    add = p.addCommands.get(1);
    d = add.solrDoc;
    f = d.getField("id");
    assertEquals("2", f.getValue());
    assertEquals(add.commitWithin, 100);
    assertFalse(add.overwrite);

    req.close();
  }

  @Test
  public void testInvalidJsonProducesBadRequestSolrException() {
    SolrQueryResponse rsp = new SolrQueryResponse();
    BufferingRequestProcessor p = new BufferingRequestProcessor(null);
    JsonLoader loader = new JsonLoader();
    String invalidJsonString = "}{";

    SolrException ex =
        expectThrows(
            SolrException.class,
            () -> {
              loader.load(req(), rsp, new ContentStreamBase.StringStream(invalidJsonString), p);
            });
    assertEquals(SolrException.ErrorCode.BAD_REQUEST.code, ex.code());
    assertTrue(ex.getMessage().contains("Cannot parse"));
    assertTrue(ex.getMessage().contains("JSON"));
  }

  public void testSimpleFormatInAdd() throws Exception {
    String str = "{'add':[{'id':'1'},{'id':'2'}]}".replace('\'', '"');
    SolrQueryRequest req = req();
    SolrQueryResponse rsp = new SolrQueryResponse();
    BufferingRequestProcessor p = new BufferingRequestProcessor(null);
    JsonLoader loader = new JsonLoader();
    loader.load(req, rsp, new ContentStreamBase.StringStream(str), p);

    assertEquals(2, p.addCommands.size());

    AddUpdateCommand add = p.addCommands.get(0);
    SolrInputDocument d = add.solrDoc;
    SolrInputField f = d.getField("id");
    assertEquals("1", f.getValue());
    assertEquals(add.commitWithin, -1);
    assertTrue(add.overwrite);

    add = p.addCommands.get(1);
    d = add.solrDoc;
    f = d.getField("id");
    assertEquals("2", f.getValue());
    assertEquals(add.commitWithin, -1);
    assertTrue(add.overwrite);

    req.close();
  }

  public void testFieldValueOrdering() throws Exception {
    final String pre = "{'add':[{'id':'1',";
    final String post = "},{'id':'2'}]}";

    // list
    checkFieldValueOrdering((pre + "'f':[45,67,89]" + post).replace('\'', '"'));
    // duplicate field name keys
    checkFieldValueOrdering((pre + "'f':45,'f':67,'f':89" + post).replace('\'', '"'));
  }

  private void checkFieldValueOrdering(String rawJson) throws Exception {
    SolrQueryRequest req = req();
    SolrQueryResponse rsp = new SolrQueryResponse();
    BufferingRequestProcessor p = new BufferingRequestProcessor(null);
    JsonLoader loader = new JsonLoader();
    loader.load(req, rsp, new ContentStreamBase.StringStream(rawJson), p);
    assertEquals(2, p.addCommands.size());

    SolrInputDocument d = p.addCommands.get(0).solrDoc;
    assertEquals(2, d.getFieldNames().size());
    assertEquals("1", d.getFieldValue("id"));
    assertArrayEquals(new Object[] {45L, 67L, 89L}, d.getFieldValues("f").toArray());

    d = p.addCommands.get(1).solrDoc;
    assertEquals(1, d.getFieldNames().size());
    assertEquals("2", d.getFieldValue("id"));

    req.close();
  }

  public void testMultipleDocsWithoutArray() throws Exception {
    String doc = "\n" + "\n" + "{\"f1\": 1111 }\n" + "\n" + "{\"f1\": 2222 }\n";
    SolrQueryRequest req = req("srcField", "_src_");
    req.getContext().put("path", "/update/json/docs");
    SolrQueryResponse rsp = new SolrQueryResponse();
    BufferingRequestProcessor p = new BufferingRequestProcessor(null);
    JsonLoader loader = new JsonLoader();
    loader.load(req, rsp, new ContentStreamBase.StringStream(doc), p);
    assertEquals(2, p.addCommands.size());
  }

  public void testJsonDocFormat() throws Exception {
    String doc;
    SolrQueryRequest req;
    SolrQueryResponse rsp;
    BufferingRequestProcessor p;
    JsonLoader loader;

    doc =
        "\n"
            + "\n"
            + "{\"bool\": true,\n"
            + " \"f0\": \"v0\",\n"
            + " \"f2\": {\n"
            + "    \t  \"boost\": 2.3,\n"
            + "    \t  \"value\": \"test\"\n"
            + "    \t   },\n"
            + "\"array\": [ \"aaa\", \"bbb\" ],\n"
            + "\"boosted\": {\n"
            + "    \t      \"boost\": 6.7,\n"
            + "    \t      \"value\": [ \"aaa\", \"bbb\" ]\n"
            + "    \t    }\n"
            + " }\n"
            + "\n"
            + "\n"
            + " {\"f1\": \"v1\",\n"
            + "  \"f2\": \"v2\",\n"
            + "   \"f3\": null\n"
            + "  }\n";
    req = req("srcField", "_src_");
    req.getContext().put("path", "/update/json/docs");
    rsp = new SolrQueryResponse();
    p = new BufferingRequestProcessor(null);
    loader = new JsonLoader();
    loader.load(req, rsp, new ContentStreamBase.StringStream(doc), p);

    assertEquals(2, p.addCommands.size());

    String content = (String) p.addCommands.get(0).solrDoc.getFieldValue("_src_");
    assertNotNull(content);
    Map<?, ?> obj = (Map<?, ?>) Utils.fromJSONString(content);
    assertEquals(Boolean.TRUE, obj.get("bool"));
    assertEquals("v0", obj.get("f0"));
    assertNotNull(obj.get("f0"));
    assertNotNull(obj.get("array"));
    assertNotNull(obj.get("boosted"));

    content = (String) p.addCommands.get(1).solrDoc.getFieldValue("_src_");
    assertNotNull(content);
    obj = (Map<?, ?>) Utils.fromJSONString(content);
    assertEquals("v1", obj.get("f1"));
    assertEquals("v2", obj.get("f2"));
    assertTrue(obj.containsKey("f3"));

    // TODO new test method
    String json = "{a:{" + "b:[{c:c1, e:e1},{c:c2, e :e2, d:{p:q}}]," + "x:y" + "}}";
    req = req("split", "/|/a/b");
    req.getContext().put("path", "/update/json/docs");
    rsp = new SolrQueryResponse();
    p = new BufferingRequestProcessor(null);
    loader = new JsonLoader();
    loader.load(req, rsp, new ContentStreamBase.StringStream(json), p);
    assertEquals(1, p.addCommands.size());
    assertEquals(
        "SolrInputDocument(fields: ["
            + "b=["
            + "SolrInputDocument(fields: [c=c1, e=e1]), "
            + "SolrInputDocument(fields: [c=c2, e=e2, d.p=q])], "
            + "a.x=y"
            + "])",
        p.addCommands.get(0).solrDoc.toString());
  }

  public void testSrcAndUniqueDocs() throws Exception {
    BufferingRequestProcessor p;
    JsonLoader loader;
    SolrQueryRequest req;
    SolrQueryResponse rsp;
    String doc;
    String content;
    Map<?, ?> src;
    doc = "[{'id':'1','a':'b'},{'id':'2','c':'d'}]".replace('\'', '"');
    boolean idOnly = random().nextBoolean();
    boolean srcField = random().nextBoolean();
    ModifiableSolrParams params = new ModifiableSolrParams();
    if (idOnly) {
      params.set("mapUniqueKeyOnly", "true");
      params.set("df", "_catch_all");
    }
    if (srcField) {
      params.set("srcField", "_src_");
    }
    req = req(params);
    req.getContext().put("path", "/update/json/docs");
    rsp = new SolrQueryResponse();
    p = new BufferingRequestProcessor(null);
    loader = new JsonLoader();
    loader.load(req, rsp, new ContentStreamBase.StringStream(doc), p);
    assertEquals(2, p.addCommands.size());

    {
      final SolrInputDocument doc1 = p.addCommands.get(0).solrDoc;
      if (srcField) {
        content = (String) doc1.getFieldValue("_src_");
        assertNotNull(content);
        src = (Map) Utils.fromJSONString(content);
        assertEquals("1", src.get("id"));
        assertEquals("b", src.get("a"));
      } else {
        assertFalse(doc1.containsKey("_src_"));
      }
      assertEquals("1", doc1.getFieldValue("id"));
      if (idOnly) {
        assertEquals(Arrays.asList("1", "b"), new ArrayList<>(doc1.getFieldValues("_catch_all")));
        assertFalse(doc1.containsKey("a"));
      } else {
        assertEquals("b", doc1.getFieldValue("a"));
        assertFalse(doc1.containsKey("_catch_all"));
      }
    }
    {
      final SolrInputDocument doc2 = p.addCommands.get(1).solrDoc;
      if (srcField) {
        content = (String) doc2.getFieldValue("_src_");
        assertNotNull(content);
        src = (Map) Utils.fromJSONString(content);
        assertEquals("2", src.get("id"));
        assertEquals("d", src.get("c"));
      } else {
        assertFalse(doc2.containsKey("_src_"));
      }
      assertEquals("2", doc2.getFieldValue("id"));
      if (idOnly) {
        assertEquals(Arrays.asList("2", "d"), new ArrayList<>(doc2.getFieldValues("_catch_all")));
        assertFalse(doc2.containsKey("c"));
      } else {
        assertEquals("d", doc2.getFieldValue("c"));
        assertFalse(doc2.containsKey("_catch_all"));
      }
    }
  }

  public void testEchoDocs() throws Exception {
    BufferingRequestProcessor p;
    JsonLoader loader;
    SolrQueryRequest req;
    SolrQueryResponse rsp;
    String doc;

    doc = "[{'id':'1'},{'id':'2'}]".replace('\'', '"');
    boolean src = random().nextBoolean();
    req = src ? req("srcField", "_src_", "echo", "true") : req("echo", "true");
    req.getContext().put("path", "/update/json/docs");
    rsp = new SolrQueryResponse();
    p = new BufferingRequestProcessor(null);
    loader = new JsonLoader();
    loader.load(req, rsp, new ContentStreamBase.StringStream(doc), p);
    assertEquals(0, p.addCommands.size());
    @SuppressWarnings("unchecked")
    final List<Map<String, String>> docs = (List<Map<String, String>>) rsp.getValues().get("docs");
    assertNotNull(docs);
    assertEquals(2, docs.size());
    {
      Map<String, String> doc1 = docs.get(0);
      assertEquals("1", doc1.get("id"));
      if (src) {
        assertEquals("{\"id\":\"1\"}", doc1.get("_src_"));
      } else {
        assertFalse(doc1.containsKey("_src_"));
      }
    }
    {
      Map<String, String> doc2 = docs.get(1);
      assertEquals("2", doc2.get("id"));
      if (src) {
        assertEquals("{\"id\":\"2\"}", doc2.get("_src_"));
      } else {
        assertFalse(doc2.containsKey("_src_"));
      }
    }
  }

  private static final String PARENT_TWO_CHILDREN_JSON =
      "{\n"
          + "  \"id\": \"1\",\n"
          + "  \"name\": \"i am the parent\",\n"
          + "  \"cat\": \"parent\",\n"
          + "  \"children\": [\n"
          + "    {\n"
          + "      \"id\": \"1.1\",\n"
          + "      \"name\": \"i am the 1st child\",\n"
          + "      \"cat\": \"child\"\n"
          + "    },\n"
          + "    {\n"
          + "      \"id\": \"1.2\",\n"
          + "      \"name\": \"i am the 2nd child\",\n"
          + "      \"cat\": \"child\",\n"
          + "      \"test_s\": \"test-new-label\",\n"
          + "      \"grandchildren\": [\n"
          + "        {\n"
          + "          \"id\": \"1.2.1\",\n"
          + "          \"name\": \"i am the grandchild\",\n"
          + "          \"cat\": \"grandchild\"\n"
          + "        }\n"
          + "      ]\n"
          + "    }\n"
          + "  ]\n"
          + "}";

  private static final String[] PARENT_TWO_CHILDREN_PARAMS =
      new String[] {
        "split",
        "/|/children|/children/grandchildren",
        "f",
        "$FQN:/**",
        "f",
        "id:/children/id",
        "f",
        "/name",
        "f",
        "/children/name",
        "f",
        "cat:/children/cat",
        "f",
        "id:/children/grandchildren/id",
        "f",
        "name:/children/grandchildren/name",
        "f",
        "cat:/children/grandchildren/cat"
      };

  @Test
  public void testFewParentsJsonDoc() throws Exception {
    String json = PARENT_TWO_CHILDREN_JSON;

    SolrQueryRequest req;
    SolrQueryResponse rsp;
    BufferingRequestProcessor p;
    JsonLoader loader; // multiple children test case
    final boolean array = random().nextBoolean();
    StringBuilder b = new StringBuilder();
    if (array) {
      b.append("[");
    }
    final int passes = atLeast(2);
    for (int i = 1; i <= passes; i++) {
      b.append(json.replace("1", "" + i));
      if (array) {
        b.append(i < passes ? "," : "]");
      }
    }

    req = req(PARENT_TWO_CHILDREN_PARAMS);
    req.getContext().put("path", "/update/json/docs");
    rsp = new SolrQueryResponse();
    p = new BufferingRequestProcessor(null);
    loader = new JsonLoader();
    loader.load(req, rsp, new ContentStreamBase.StringStream(b.toString()), p);
    for (int i = 1; i <= passes; i++) {
      final int ii = i;
      UnaryOperator<String> s = (v) -> v.replace("1", "" + ii);
      final SolrInputDocument parent = p.addCommands.get(i - 1).solrDoc;
      assertOnlyValue(s.apply("1"), parent, "id");
      assertOnlyValue("i am the parent", parent, "name");
      assertOnlyValue("parent", parent, "cat");

      @SuppressWarnings({"unchecked"})
      List<SolrInputDocument> childDocs1 = (List) ((parent.getField("children")).getValue());

      assertEquals(2, childDocs1.size());
      {
        final SolrInputDocument child1 = childDocs1.get(0);
        assertOnlyValue(s.apply("1.1"), child1, "id");
        assertOnlyValue(s.apply("i am the 1st child"), child1, "name");
        assertOnlyValue("child", child1, "cat");
      }
      {
        final SolrInputDocument child2 = childDocs1.get(1);
        assertOnlyValue(s.apply("1.2"), child2, "id");
        assertOnlyValue("i am the 2nd child", child2, "name");
        assertOnlyValue("test-new-label", child2, "test_s");
        assertOnlyValue("child", child2, "cat");

        @SuppressWarnings({"unchecked"})
        List<SolrInputDocument> childDocs2 = (List) ((child2.getField("grandchildren")).getValue());

        assertEquals(1, childDocs2.size());
        final SolrInputDocument grandChild = childDocs2.get(0);
        assertOnlyValue(s.apply("1.2.1"), grandChild, "id");
        assertOnlyValue("i am the grandchild", grandChild, "name");
        assertOnlyValue("grandchild", grandChild, "cat");
      }
    }
  }

  private static void assertOnlyValue(String expected, SolrInputDocument doc, String field) {
    assertEquals(List.of(expected), List.copyOf(doc.getFieldValues(field)));
  }

  public void testAtomicUpdateFieldValue() throws Exception {
    String str = "[{'id':'1', 'val_s':{'add':'foo'}}]".replace('\'', '"');
    SolrQueryRequest req = req();
    SolrQueryResponse rsp = new SolrQueryResponse();
    BufferingRequestProcessor p = new BufferingRequestProcessor(null);
    JsonLoader loader = new JsonLoader();
    loader.load(req, rsp, new ContentStreamBase.StringStream(str), p);

    assertEquals(1, p.addCommands.size());

    AddUpdateCommand add = p.addCommands.get(0);
    assertEquals(add.commitWithin, -1);
    assertTrue(add.overwrite);
    assertEquals("SolrInputDocument(fields: [id=1, val_s={add=foo}])", add.solrDoc.toString());

    req.close();
  }

  @Test
  public void testNullValues() throws Exception {
    updateJ(
        json("[{'id':'10','foo_s':null,'foo2_s':['hi',null,'there']}]"), params("commit", "true"));
    assertJQ(
        req("q", "id:10", "fl", "foo_s,foo2_s"), "/response/docs/[0]=={'foo2_s':['hi','there']}");
  }

  @Test
  public void testBooleanValuesInAdd() throws Exception {
    String str = "{'add':[{'id':'1','b1':true,'b2':false,'b3':[false,true]}]}".replace('\'', '"');
    SolrQueryRequest req = req();
    SolrQueryResponse rsp = new SolrQueryResponse();
    BufferingRequestProcessor p = new BufferingRequestProcessor(null);
    JsonLoader loader = new JsonLoader();
    loader.load(req, rsp, new ContentStreamBase.StringStream(str), p);

    assertEquals(1, p.addCommands.size());

    AddUpdateCommand add = p.addCommands.get(0);
    SolrInputDocument d = add.solrDoc;
    SolrInputField f = d.getField("b1");
    assertEquals(Boolean.TRUE, f.getValue());
    f = d.getField("b2");
    assertEquals(Boolean.FALSE, f.getValue());
    f = d.getField("b3");
    assertEquals(2, ((List) f.getValue()).size());
    assertEquals(Boolean.FALSE, ((List) f.getValue()).get(0));
    assertEquals(Boolean.TRUE, ((List) f.getValue()).get(1));

    req.close();
  }

  @Test
  public void testIntegerValuesInAdd() throws Exception {
    String str = "{'add':[{'id':'1','i1':256,'i2':-5123456789,'i3':[0,1]}]}".replace('\'', '"');
    SolrQueryRequest req = req();
    SolrQueryResponse rsp = new SolrQueryResponse();
    BufferingRequestProcessor p = new BufferingRequestProcessor(null);
    JsonLoader loader = new JsonLoader();
    loader.load(req, rsp, new ContentStreamBase.StringStream(str), p);

    assertEquals(1, p.addCommands.size());

    AddUpdateCommand add = p.addCommands.get(0);
    SolrInputDocument d = add.solrDoc;
    SolrInputField f = d.getField("i1");
    assertEquals(256L, f.getValue());
    f = d.getField("i2");
    assertEquals(-5123456789L, f.getValue());
    f = d.getField("i3");
    assertEquals(2, ((List) f.getValue()).size());
    assertEquals(0L, ((List) f.getValue()).get(0));
    assertEquals(1L, ((List) f.getValue()).get(1));

    req.close();
  }

  @Test
  public void testDecimalValuesInAdd() throws Exception {
    String str =
        "{'add':[{'id':'1','d1':256.78,'d2':-5123456789.0,'d3':0.0,'d3':1.0,'d4':1.7E-10}]}"
            .replace('\'', '"');
    SolrQueryRequest req = req();
    SolrQueryResponse rsp = new SolrQueryResponse();
    BufferingRequestProcessor p = new BufferingRequestProcessor(null);
    JsonLoader loader = new JsonLoader();
    loader.load(req, rsp, new ContentStreamBase.StringStream(str), p);

    assertEquals(1, p.addCommands.size());

    AddUpdateCommand add = p.addCommands.get(0);
    SolrInputDocument d = add.solrDoc;
    SolrInputField f = d.getField("d1");
    assertEquals(256.78, f.getValue());
    f = d.getField("d2");
    assertEquals(-5123456789.0, f.getValue());
    f = d.getField("d3");
    assertEquals(2, ((List) f.getValue()).size());
    assertTrue(((List) f.getValue()).contains(0.0));
    assertTrue(((List) f.getValue()).contains(1.0));
    f = d.getField("d4");
    assertEquals(1.7E-10, f.getValue());

    req.close();
  }

  @Test
  public void testBigDecimalValuesInAdd() throws Exception {
    String str =
        ("{'add':[{'id':'1','bd1':0.12345678901234567890123456789012345,"
                + "'bd2':12345678901234567890.12345678901234567890,'bd3':0.012345678901234567890123456789012345,"
                + "'bd3':123456789012345678900.012345678901234567890}]}")
            .replace('\'', '"');
    SolrQueryRequest req = req();
    SolrQueryResponse rsp = new SolrQueryResponse();
    BufferingRequestProcessor p = new BufferingRequestProcessor(null);
    JsonLoader loader = new JsonLoader();
    loader.load(req, rsp, new ContentStreamBase.StringStream(str), p);

    assertEquals(1, p.addCommands.size());

    AddUpdateCommand add = p.addCommands.get(0);
    SolrInputDocument d = add.solrDoc;
    SolrInputField f = d.getField("bd1");
    assertTrue(f.getValue() instanceof String);
    assertEquals("0.12345678901234567890123456789012345", f.getValue());
    f = d.getField("bd2");
    assertTrue(f.getValue() instanceof String);
    assertEquals("12345678901234567890.12345678901234567890", f.getValue());
    f = d.getField("bd3");
    assertEquals(2, ((List) f.getValue()).size());
    assertTrue(((List) f.getValue()).contains("0.012345678901234567890123456789012345"));
    assertTrue(((List) f.getValue()).contains("123456789012345678900.012345678901234567890"));

    req.close();
  }

  @Test
  public void testBigIntegerValuesInAdd() throws Exception {
    String str =
        ("{'add':[{'id':'1','bi1':123456789012345678901,'bi2':1098765432109876543210,"
                + "'bi3':[1234567890123456789012,10987654321098765432109]}]}")
            .replace('\'', '"');
    SolrQueryRequest req = req();
    SolrQueryResponse rsp = new SolrQueryResponse();
    BufferingRequestProcessor p = new BufferingRequestProcessor(null);
    JsonLoader loader = new JsonLoader();
    loader.load(req, rsp, new ContentStreamBase.StringStream(str), p);

    assertEquals(1, p.addCommands.size());

    AddUpdateCommand add = p.addCommands.get(0);
    SolrInputDocument d = add.solrDoc;
    SolrInputField f = d.getField("bi1");
    assertTrue(f.getValue() instanceof String);
    assertEquals("123456789012345678901", f.getValue());
    f = d.getField("bi2");
    assertTrue(f.getValue() instanceof String);
    assertEquals("1098765432109876543210", f.getValue());
    f = d.getField("bi3");
    assertEquals(2, ((List) f.getValue()).size());
    assertTrue(((List) f.getValue()).contains("1234567890123456789012"));
    assertTrue(((List) f.getValue()).contains("10987654321098765432109"));

    req.close();
  }

  @Test
  public void testAddNonStringValues() throws Exception {
    // BigInteger and BigDecimal should be typed as strings, since there is no direct support for
    // them
    updateJ(
        json(
            "[{'id':'1','boolean_b':false,'long_l':19,'double_d':18.6,'big_integer_s':12345678901234567890,"
                + "      'big_decimal_s':0.1234567890123456789012345}]"),
        params("commit", "true"));
    assertJQ(
        req("q", "id:1", "fl", "boolean_b,long_l,double_d,big_integer_s,big_decimal_s"),
        "/response/docs/[0]=={'boolean_b':[false],'long_l':[19],'double_d':[18.6],"
            + "'big_integer_s':['12345678901234567890'],"
            + "'big_decimal_s':['0.1234567890123456789012345']}]}");
  }

  @Test
  public void testAddBigIntegerValueToTrieField() {
    // Adding a BigInteger to a long field should fail
    // BigInteger.longValue() returns only the low-order 64 bits.

    ignoreException("big_integer_t");

    SolrException ex =
        expectThrows(
            SolrException.class,
            () -> {
              updateJ(json("[{'id':'1','big_integer_tl':12345678901234567890}]"), null);
            });
    assertTrue(ex.getCause() instanceof NumberFormatException);

    // Adding a BigInteger to an integer field should fail
    // BigInteger.intValue() returns only the low-order 32 bits.
    ex =
        expectThrows(
            SolrException.class,
            () -> {
              updateJ(json("[{'id':'1','big_integer_ti':12345678901234567890}]"), null);
            });
    assertTrue(ex.getCause() instanceof NumberFormatException);

    unIgnoreException("big_integer_t");
  }

  @Test
  public void testAddBigDecimalValueToTrieField() throws Exception {
    // Adding a BigDecimal to a double field should succeed by reducing precision
    updateJ(
        json("[{'id':'1','big_decimal_td':100000000000000000000000000001234567890.0987654321}]"),
        params("commit", "true"));
    assertJQ(
        req("q", "id:1", "fl", "big_decimal_td"),
        "/response/docs/[0]=={'big_decimal_td':[1.0E38]}");

    // Adding a BigDecimal to a float field should succeed by reducing precision
    updateJ(
        json("[{'id':'2','big_decimal_tf':100000000000000000000000000001234567890.0987654321}]"),
        params("commit", "true"));
    assertJQ(
        req("q", "id:2", "fl", "big_decimal_tf"),
        "/response/docs/[0]=={'big_decimal_tf':[1.0E38]}");
  }

  // The delete operation syntax was extended for simplification in 4.0
  @Test
  public void testDeleteSyntax() throws Exception {
    String str =
        "{'delete':10"
            + "\n ,'delete':'20'"
            + "\n ,'delete':['30','40']"
            + "\n ,'delete':{'id':50, '_version_':12345}"
            + "\n ,'delete':[{'id':60, '_version_':67890}, {'id':70, '_version_':77777}, {'query':'id:80', '_version_':88888}]"
            + "\n ,'delete':{'id':90, '_route_':'shard1', '_version_':88888}"
            + "\n}\n";
    str = str.replace('\'', '"');
    SolrQueryRequest req = req();
    SolrQueryResponse rsp = new SolrQueryResponse();
    BufferingRequestProcessor p = new BufferingRequestProcessor(null);
    JsonLoader loader = new JsonLoader();
    loader.load(req, rsp, new ContentStreamBase.StringStream(str), p);

    // DELETE COMMANDS
    assertEquals(9, p.deleteCommands.size());
    DeleteUpdateCommand delete = p.deleteCommands.get(0);
    assertEquals(delete.id, "10");
    assertNull(delete.query);
    assertEquals(delete.commitWithin, -1);

    delete = p.deleteCommands.get(1);
    assertEquals(delete.id, "20");
    assertNull(delete.query);
    assertEquals(delete.commitWithin, -1);

    delete = p.deleteCommands.get(2);
    assertEquals(delete.id, "30");
    assertNull(delete.query);
    assertEquals(delete.commitWithin, -1);

    delete = p.deleteCommands.get(3);
    assertEquals(delete.id, "40");
    assertNull(delete.query);
    assertEquals(delete.commitWithin, -1);

    delete = p.deleteCommands.get(4);
    assertEquals(delete.id, "50");
    assertNull(delete.query);
    assertEquals(delete.getVersion(), 12345L);

    delete = p.deleteCommands.get(5);
    assertEquals(delete.id, "60");
    assertNull(delete.query);
    assertEquals(delete.getVersion(), 67890L);

    delete = p.deleteCommands.get(6);
    assertEquals(delete.id, "70");
    assertNull(delete.query);
    assertEquals(delete.getVersion(), 77777L);

    delete = p.deleteCommands.get(7);
    assertNull(delete.id);
    assertEquals(delete.query, "id:80");
    assertEquals(delete.getVersion(), 88888L);

    delete = p.deleteCommands.get(8);
    assertEquals(delete.id, "90");
    assertNull(delete.query);
    assertEquals(delete.getRoute(), "shard1");
    assertEquals(delete.getVersion(), 88888L);

    req.close();
  }

  private static final String SIMPLE_ANON_CHILD_DOCS_JSON =
      "{\n"
          + "    \"add\": {\n"
          + "        \"doc\": {\n"
          + "            \"id\": \"1\",\n"
          + "            \"_childDocuments_\": [\n"
          + "                {\n"
          + "                    \"id\": \"2\"\n"
          + "                },\n"
          + "                {\n"
          + "                    \"id\": \"3\",\n"
          + "                    \"foo_i\": [666,777]\n"
          + "                }\n"
          + "            ]\n"
          + "        }\n"
          + "    }\n"
          + "}";

  @Test
  public void testSimpleAnonymousChildDocs() throws Exception {
    checkTwoAnonymousChildDocs(SIMPLE_ANON_CHILD_DOCS_JSON, true);
  }

  @Test
  public void testSimpleChildDocs() throws Exception {
    checkTwoAnonymousChildDocs(SIMPLE_ANON_CHILD_DOCS_JSON, false);
  }

  private static final String DUP_KEYS_ANON_CHILD_DOCS_JSON =
      "{\n"
          + "    \"add\": {\n"
          + "        \"doc\": {\n"
          + "            \"_childDocuments_\": [\n"
          + "                {\n"
          + "                    \"id\": \"2\"\n"
          + "                }\n"
          + "            ],\n"
          + "            \"id\": \"1\",\n"
          + "            \"_childDocuments_\": [\n"
          + "                {\n"
          + "                    \"id\": \"3\",\n"
          + "                    \"foo_i\": 666,\n"
          + "                    \"foo_i\": 777\n"
          + "                }\n"
          + "            ]\n"
          + "        }\n"
          + "    }\n"
          + "}";

  @Test
  public void testDupKeysAnonymousChildDocs() throws Exception {
    checkTwoAnonymousChildDocs(DUP_KEYS_ANON_CHILD_DOCS_JSON, true);
  }

  @Test
  public void testDupKeysChildDocs() throws Exception {
    checkTwoAnonymousChildDocs(DUP_KEYS_ANON_CHILD_DOCS_JSON, false);
  }

  @Test
  public void testChildDocWithoutId() throws Exception {
    final String json = DUP_KEYS_ANON_CHILD_DOCS_JSON.replace("\"id\": \"3\",\n", "");
    assertFalse(json.equals(DUP_KEYS_ANON_CHILD_DOCS_JSON));
    checkTwoAnonymousChildDocs(json, false);
  }

  // rawJsonStr has "_childDocuments_" key.  if anonChildDocs then we want to test with something
  // else.
  private void checkTwoAnonymousChildDocs(String rawJsonStr, boolean anonChildDocs)
      throws Exception {
    if (!anonChildDocs) {
      rawJsonStr = rawJsonStr.replace("_childDocuments_", "childLabel");
    }
    SolrQueryRequest req = req("commit", "true");
    SolrQueryResponse rsp = new SolrQueryResponse();
    BufferingRequestProcessor p = new BufferingRequestProcessor(null);
    JsonLoader loader = new JsonLoader();
    loader.load(req, rsp, new ContentStreamBase.StringStream(rawJsonStr), p);

    assertEquals(1, p.addCommands.size());

    AddUpdateCommand add = p.addCommands.get(0);
    SolrInputDocument d = add.solrDoc;
    SolrInputField f = d.getField("id");
    assertEquals("1", f.getValue());

    SolrInputDocument cd;
    if (anonChildDocs) {
      cd = d.getChildDocuments().get(0);
    } else {
      cd = (SolrInputDocument) (d.getField("childLabel")).getFirstValue();
    }
    SolrInputField cf = cd.getField("id");
    assertEquals("2", cf.getValue());

    if (anonChildDocs) {
      cd = d.getChildDocuments().get(1);
    } else {
      cd = (SolrInputDocument) ((List) (d.getField("childLabel")).getValue()).get(1);
    }
    cf = cd.getField("id");
    if (rawJsonStr.contains("\"3\"")) {
      assertEquals("3", cf.getValue());
    } else { // ID 3 was removed previously to test we don't need an ID to have a child doc
      assertNull("child doc should have no ID", cf);
    }
    cf = cd.getField("foo_i");
    assertEquals(2, cf.getValueCount());

    assertEquals(new Object[] {666L, 777L}, cf.getValues().toArray());

    req.close();
  }

  @Test
  public void testEmptyAnonymousChildDocs() throws Exception {
    String str =
        "{\n"
            + "    \"add\": {\n"
            + "        \"doc\": {\n"
            + "            \"id\": \"1\",\n"
            + "            \"_childDocuments_\": []\n"
            + "        }\n"
            + "    }\n"
            + "}";
    SolrQueryRequest req = req("commit", "true");
    SolrQueryResponse rsp = new SolrQueryResponse();
    BufferingRequestProcessor p = new BufferingRequestProcessor(null);
    JsonLoader loader = new JsonLoader();
    loader.load(req, rsp, new ContentStreamBase.StringStream(str), p);

    assertEquals(1, p.addCommands.size());

    AddUpdateCommand add = p.addCommands.get(0);
    SolrInputDocument d = add.solrDoc;
    SolrInputField f = d.getField("id");
    assertEquals("1", f.getValue());
    List<SolrInputDocument> cd = d.getChildDocuments();
    assertNull(cd);

    req.close();
  }

  @Test
  public void testAnonymousGrandChildDocs() throws Exception {
    String str =
        "{\n"
            + "    \"add\": {\n"
            + "        \"doc\": {\n"
            + "            \"id\": \"1\",\n"
            + "            \"_childDocuments_\": [\n"
            + "                {\n"
            + "                    \"id\": \"2\",\n"
            + "                    \"_childDocuments_\": [\n"
            + "                        {\n"
            + "                           \"id\": \"4\",\n"
            + "                           \"foo_s\": \"Baz\"\n"
            + "                        }\n"
            + "                    ],\n"
            + "                    \"foo_s\": \"Yaz\"\n"
            + "                },\n"
            + "                {\n"
            + "                    \"id\": \"3\",\n"
            + "                    \"foo_s\": \"Bar\"\n"
            + "                }\n"
            + "            ]\n"
            + "        }\n"
            + "    }\n"
            + "}";

    SolrQueryRequest req = req("commit", "true");
    SolrQueryResponse rsp = new SolrQueryResponse();
    BufferingRequestProcessor p = new BufferingRequestProcessor(null);
    JsonLoader loader = new JsonLoader();
    loader.load(req, rsp, new ContentStreamBase.StringStream(str), p);

    assertEquals(1, p.addCommands.size());

    AddUpdateCommand add = p.addCommands.get(0);
    SolrInputDocument one = add.solrDoc;
    assertEquals("1", one.getFieldValue("id"));

    SolrInputDocument two = one.getChildDocuments().get(0);
    assertEquals("2", two.getFieldValue("id"));
    assertEquals("Yaz", two.getFieldValue("foo_s"));

    SolrInputDocument four = two.getChildDocuments().get(0);
    assertEquals("4", four.getFieldValue("id"));
    assertEquals("Baz", four.getFieldValue("foo_s"));

    SolrInputDocument three = one.getChildDocuments().get(1);
    assertEquals("3", three.getFieldValue("id"));
    assertEquals("Bar", three.getFieldValue("foo_s"));

    req.close();
  }

  @Test
  public void testChildDocs() throws Exception {
    String str =
        "{\n"
            + "    \"add\": {\n"
            + "        \"doc\": {\n"
            + "            \"id\": \"1\",\n"
            + "            \"children\": [\n"
            + "                {\n"
            + "                    \"id\": \"2\",\n"
            + "                    \"foo_s\": \"Yaz\"\n"
            + "                },\n"
            + "                {\n"
            + "                    \"id\": \"3\",\n"
            + "                    \"foo_s\": \"Bar\"\n"
            + "                }\n"
            + "            ]\n"
            + "        }\n"
            + "    }\n"
            + "}";

    SolrQueryRequest req = req("commit", "true");
    SolrQueryResponse rsp = new SolrQueryResponse();
    BufferingRequestProcessor p = new BufferingRequestProcessor(null);
    JsonLoader loader = new JsonLoader();
    loader.load(req, rsp, new ContentStreamBase.StringStream(str), p);

    assertEquals(1, p.addCommands.size());

    AddUpdateCommand add = p.addCommands.get(0);
    SolrInputDocument one = add.solrDoc;
    assertEquals("1", one.getFieldValue("id"));

    @SuppressWarnings({"unchecked"})
    List<SolrInputDocument> children = (List) one.getFieldValues("children");
    SolrInputDocument two = children.get(0);
    assertEquals("2", two.getFieldValue("id"));
    assertEquals("Yaz", two.getFieldValue("foo_s"));

    SolrInputDocument three = children.get(1);
    assertEquals("3", three.getFieldValue("id"));
    assertEquals("Bar", three.getFieldValue("foo_s"));

    req.close();
  }

  @Test
  public void testSingleRelationalChildDoc() throws Exception {
    String str =
        "{\n"
            + "    \"add\": {\n"
            + "        \"doc\": {\n"
            + "            \"id\": \"1\",\n"
            + "            \"child1\": \n"
            + "                {\n"
            + "                    \"id\": \"2\",\n"
            + "                    \"foo_s\": \"Yaz\"\n"
            + "                },\n"
            + "        }\n"
            + "    }\n"
            + "}";

    SolrQueryRequest req = req("commit", "true");
    SolrQueryResponse rsp = new SolrQueryResponse();
    BufferingRequestProcessor p = new BufferingRequestProcessor(null);
    JsonLoader loader = new JsonLoader();
    loader.load(req, rsp, new ContentStreamBase.StringStream(str), p);

    assertEquals(1, p.addCommands.size());

    AddUpdateCommand add = p.addCommands.get(0);
    SolrInputDocument one = add.solrDoc;
    assertEquals("1", one.getFieldValue("id"));

    assertTrue(one.keySet().contains("child1"));

    SolrInputDocument two = (SolrInputDocument) one.getField("child1").getValue();
    assertEquals("2", two.getFieldValue("id"));
    assertEquals("Yaz", two.getFieldValue("foo_s"));

    req.close();
  }

  @Test
  public void JSONLoader_denseVector_shouldIndexCorrectly() throws Exception {
    updateJ(json("[{'id':'888','vector':[1.8,2.8,3.8,4.8]}]"), params("commit", "true"));

    assertJQ(
        req("q", "id:888", "fl", "vector"), "/response/docs/[0]=={'vector':[1.8,2.8,3.8,4.8]}");
  }
}
